@@ -0,0 +1,128 @@ |
||
1 |
+module Agents |
|
2 |
+ class BeeperAgent < Agent |
|
3 |
+ cannot_be_scheduled! |
|
4 |
+ cannot_create_events! |
|
5 |
+ |
|
6 |
+ description <<-MD |
|
7 |
+ Beeper agent sends messages to Beeper app on your mobile device via Push notifications. |
|
8 |
+ |
|
9 |
+ You need a Beeper Application ID (`app_id`), Beeper REST API Key (`api_key`) and Beeper Sender ID (`sender_id`) [https://beeper.io](https://beeper.io) |
|
10 |
+ |
|
11 |
+ You have to provide phone number (`phone`) of the recipient which have a mobile device with Beeper installed, or a `group_id` – Beeper Group ID |
|
12 |
+ |
|
13 |
+ Also you have to provide a message `type` which has to be `message`, `image`, `event`, `location` or `task`. |
|
14 |
+ |
|
15 |
+ Depending on message type you have to provide additional fields: |
|
16 |
+ |
|
17 |
+ ##### Message |
|
18 |
+ * `text` – **required** |
|
19 |
+ |
|
20 |
+ ##### Image |
|
21 |
+ * `image` – **required** (Image URL or Base64-encoded image) |
|
22 |
+ * `text` – optional |
|
23 |
+ |
|
24 |
+ ##### Event |
|
25 |
+ * `text` – **required** |
|
26 |
+ * `start_time` – **required** (Corresponding to ISO 8601) |
|
27 |
+ * `end_time` – optional (Corresponding to ISO 8601) |
|
28 |
+ |
|
29 |
+ ##### Location |
|
30 |
+ * `latitude` – **required** |
|
31 |
+ * `longitude` – **required** |
|
32 |
+ * `text` – optional |
|
33 |
+ |
|
34 |
+ ##### Task |
|
35 |
+ * `text` – **required** |
|
36 |
+ |
|
37 |
+ You can see additional documentation at [Beeper website](https://beeper.io/docs) |
|
38 |
+ MD |
|
39 |
+ |
|
40 |
+ BASE_URL = 'https://api.beeper.io/api' |
|
41 |
+ |
|
42 |
+ TYPE_ATTRIBUTES = { |
|
43 |
+ 'message' => %w(text), |
|
44 |
+ 'image' => %w(text image), |
|
45 |
+ 'event' => %w(text start_time end_time), |
|
46 |
+ 'location' => %w(text latitude longitude), |
|
47 |
+ 'task' => %w(text) |
|
48 |
+ } |
|
49 |
+ |
|
50 |
+ MESSAGE_TYPES = TYPE_ATTRIBUTES.keys |
|
51 |
+ |
|
52 |
+ TYPE_REQUIRED_ATTRIBUTES = { |
|
53 |
+ 'message' => %w(text), |
|
54 |
+ 'image' => %w(image), |
|
55 |
+ 'event' => %w(text start_time), |
|
56 |
+ 'location' => %w(latitude longitude), |
|
57 |
+ 'task' => %w(text) |
|
58 |
+ } |
|
59 |
+ |
|
60 |
+ def default_options |
|
61 |
+ { |
|
62 |
+ 'type' => 'message', |
|
63 |
+ 'app_id' => '', |
|
64 |
+ 'api_key' => '', |
|
65 |
+ 'sender_id' => '', |
|
66 |
+ 'phone' => '', |
|
67 |
+ 'text' => '{{title}}' |
|
68 |
+ } |
|
69 |
+ end |
|
70 |
+ |
|
71 |
+ def validate_options |
|
72 |
+ %w(app_id api_key sender_id type).each do |attr| |
|
73 |
+ errors.add(:base, "you need to specify a #{attr}") if options[attr].blank? |
|
74 |
+ end |
|
75 |
+ |
|
76 |
+ if options['type'].in?(MESSAGE_TYPES) |
|
77 |
+ required_attributes = TYPE_REQUIRED_ATTRIBUTES[options['type']] |
|
78 |
+ if required_attributes.any? { |attr| options[attr].blank? } |
|
79 |
+ errors.add(:base, "you need to specify a #{required_attributes.join(', ')}") |
|
80 |
+ end |
|
81 |
+ else |
|
82 |
+ errors.add(:base, 'you need to specify a valid message type') |
|
83 |
+ end |
|
84 |
+ |
|
85 |
+ unless options['group_id'].blank? ^ options['phone'].blank? |
|
86 |
+ errors.add(:base, 'you need to specify a phone or group_id') |
|
87 |
+ end |
|
88 |
+ end |
|
89 |
+ |
|
90 |
+ def working? |
|
91 |
+ received_event_without_error? && !recent_error_logs? |
|
92 |
+ end |
|
93 |
+ |
|
94 |
+ def receive(incoming_events) |
|
95 |
+ incoming_events.each do |event| |
|
96 |
+ send_message(event) |
|
97 |
+ end |
|
98 |
+ end |
|
99 |
+ |
|
100 |
+ def send_message(event) |
|
101 |
+ mo = interpolated(event) |
|
102 |
+ begin |
|
103 |
+ response = HTTParty.post(endpoint_for(mo['type']), body: payload_for(mo), headers: headers) |
|
104 |
+ error(response.body) if response.code != 201 |
|
105 |
+ rescue HTTParty::Error => e |
|
106 |
+ error(e.message) |
|
107 |
+ end |
|
108 |
+ end |
|
109 |
+ |
|
110 |
+ private |
|
111 |
+ |
|
112 |
+ def headers |
|
113 |
+ { |
|
114 |
+ 'X-Beeper-Application-Id' => options['app_id'], |
|
115 |
+ 'X-Beeper-REST-API-Key' => options['api_key'], |
|
116 |
+ 'Content-Type' => 'application/json' |
|
117 |
+ } |
|
118 |
+ end |
|
119 |
+ |
|
120 |
+ def payload_for(mo) |
|
121 |
+ mo.slice(*TYPE_ATTRIBUTES[mo['type']], 'sender_id', 'phone', 'group_id').to_json |
|
122 |
+ end |
|
123 |
+ |
|
124 |
+ def endpoint_for(type) |
|
125 |
+ "#{BASE_URL}/#{type}s.json" |
|
126 |
+ end |
|
127 |
+ end |
|
128 |
+end |
@@ -0,0 +1,145 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+ |
|
4 |
+describe Agents::BeeperAgent do |
|
5 |
+ let(:base_params) { |
|
6 |
+ { |
|
7 |
+ 'type' => 'message', |
|
8 |
+ 'app_id' => 'some-app-id', |
|
9 |
+ 'api_key' => 'some-api-key', |
|
10 |
+ 'sender_id' => 'sender-id', |
|
11 |
+ 'phone' => '+111111111111', |
|
12 |
+ 'text' => 'Some text' |
|
13 |
+ } |
|
14 |
+ } |
|
15 |
+ |
|
16 |
+ subject { |
|
17 |
+ agent = described_class.new(name: 'beeper-agent', options: base_params) |
|
18 |
+ agent.user = users(:jane) |
|
19 |
+ agent.save! and return agent |
|
20 |
+ } |
|
21 |
+ |
|
22 |
+ context 'validation' do |
|
23 |
+ it 'valid' do |
|
24 |
+ expect(subject).to be_valid |
|
25 |
+ end |
|
26 |
+ |
|
27 |
+ [:type, :app_id, :api_key, :sender_id].each do |attr| |
|
28 |
+ it "invalid without #{attr}" do |
|
29 |
+ subject.options[attr] = nil |
|
30 |
+ expect(subject).not_to be_valid |
|
31 |
+ end |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ it 'invalid with group_id and phone' do |
|
35 |
+ subject.options['group_id'] ='some-group-id' |
|
36 |
+ expect(subject).not_to be_valid |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ context '#message' do |
|
40 |
+ it 'requires text' do |
|
41 |
+ subject.options[:text] = nil |
|
42 |
+ expect(subject).not_to be_valid |
|
43 |
+ end |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ context '#image' do |
|
47 |
+ before(:each) do |
|
48 |
+ subject.options[:type] = 'image' |
|
49 |
+ end |
|
50 |
+ |
|
51 |
+ it 'invalid without image' do |
|
52 |
+ expect(subject).not_to be_valid |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ it 'valid with image' do |
|
56 |
+ subject.options[:image] = 'some-url' |
|
57 |
+ expect(subject).to be_valid |
|
58 |
+ end |
|
59 |
+ end |
|
60 |
+ |
|
61 |
+ context '#event' do |
|
62 |
+ before(:each) do |
|
63 |
+ subject.options[:type] = 'event' |
|
64 |
+ end |
|
65 |
+ |
|
66 |
+ it 'invalid without start_time' do |
|
67 |
+ expect(subject).not_to be_valid |
|
68 |
+ end |
|
69 |
+ |
|
70 |
+ it 'valid with start_time' do |
|
71 |
+ subject.options[:start_time] = Time.now |
|
72 |
+ expect(subject).to be_valid |
|
73 |
+ end |
|
74 |
+ end |
|
75 |
+ |
|
76 |
+ context '#location' do |
|
77 |
+ before(:each) do |
|
78 |
+ subject.options[:type] = 'location' |
|
79 |
+ end |
|
80 |
+ |
|
81 |
+ it 'invalid without latitude and longitude' do |
|
82 |
+ expect(subject).not_to be_valid |
|
83 |
+ end |
|
84 |
+ |
|
85 |
+ it 'valid with latitude and longitude' do |
|
86 |
+ subject.options[:latitude] = 15.0 |
|
87 |
+ subject.options[:longitude] = 16.0 |
|
88 |
+ expect(subject).to be_valid |
|
89 |
+ end |
|
90 |
+ end |
|
91 |
+ |
|
92 |
+ context '#task' do |
|
93 |
+ before(:each) do |
|
94 |
+ subject.options[:type] = 'task' |
|
95 |
+ end |
|
96 |
+ |
|
97 |
+ it 'valid with text' do |
|
98 |
+ expect(subject).to be_valid |
|
99 |
+ end |
|
100 |
+ end |
|
101 |
+ end |
|
102 |
+ |
|
103 |
+ context 'payload_for' do |
|
104 |
+ it 'removes unwanted attributes' do |
|
105 |
+ result = subject.send(:payload_for, {'type' => 'message', 'text' => 'text', |
|
106 |
+ 'sender_id' => 'sender', 'phone' => '+1', 'random_attribute' => 'unwanted'}) |
|
107 |
+ expect(result).to eq('{"text":"text","sender_id":"sender","phone":"+1"}') |
|
108 |
+ end |
|
109 |
+ end |
|
110 |
+ |
|
111 |
+ context 'headers' do |
|
112 |
+ it 'sets X-Beeper-Application-Id header with app_id' do |
|
113 |
+ expect(subject.send(:headers)['X-Beeper-Application-Id']).to eq(base_params['app_id']) |
|
114 |
+ end |
|
115 |
+ |
|
116 |
+ it 'sets X-Beeper-REST-API-Key header with api_key' do |
|
117 |
+ expect(subject.send(:headers)['X-Beeper-REST-API-Key']).to eq(base_params['api_key']) |
|
118 |
+ end |
|
119 |
+ |
|
120 |
+ it 'sets Content-Type' do |
|
121 |
+ expect(subject.send(:headers)['Content-Type']).to eq('application/json') |
|
122 |
+ end |
|
123 |
+ end |
|
124 |
+ |
|
125 |
+ context 'endpoint_for' do |
|
126 |
+ it 'returns valid URL for message' do |
|
127 |
+ expect(subject.send(:endpoint_for, 'message')).to eq('https://api.beeper.io/api/messages.json') |
|
128 |
+ end |
|
129 |
+ |
|
130 |
+ it 'returns valid URL for image' do |
|
131 |
+ expect(subject.send(:endpoint_for, 'image')).to eq('https://api.beeper.io/api/images.json') |
|
132 |
+ end |
|
133 |
+ |
|
134 |
+ it 'returns valid URL for event' do |
|
135 |
+ expect(subject.send(:endpoint_for, 'event')).to eq('https://api.beeper.io/api/events.json') |
|
136 |
+ end |
|
137 |
+ |
|
138 |
+ it 'returns valid URL for location' do |
|
139 |
+ expect(subject.send(:endpoint_for, 'location')).to eq('https://api.beeper.io/api/locations.json') |
|
140 |
+ end |
|
141 |
+ it 'returns valid URL for task' do |
|
142 |
+ expect(subject.send(:endpoint_for, 'task')).to eq('https://api.beeper.io/api/tasks.json') |
|
143 |
+ end |
|
144 |
+ end |
|
145 |
+end |